查看原文
其他

面试官:你工作三年了,还是不懂String!

以下文章来源于Java剑主 ,作者剑主

前语:不要为了读文章而读文章,一定要带着问题来读文章,勤思考。

来源:http://1t.click/aktS


# switch对String的支持


在介绍string的字符串操作之前,先说说switch对String的支持,大家都知道在Jdk1.7之前switch只能局限于int 、short 、byte 、char四类做判断条件。

而在JVM内部实际大部分字节码指令只有int类型的版本,在使用switch的时候,如果是非int型,会先转为int型,再进行条件判断。

Java1.7的switch增加了对String的支持,可String并不能直接转为int型,这是怎么做到的?

在switch中使用String:
public class Example { public void test(String status) { switch (status) { case "wo": break; case "shi": break; case "jian": break; case "zhu": break; default: break; } }}

对上面一段代码进行反编译:
public class Example{ public void test(String status) { byte var3 = -1; switch(status.hashCode()) { case 3800: if (status.equals("wo")) { var3 = 0; } break; case 113844: if (status.equals("shi")) { var3 = 1; } break; case 120583: if (status.equals("zhu")) { var3 = 3; } break; case 3261868: if (status.equals("jian")) { var3 = 2; } }}

switch比较的是字符串常量的哈希值(int类型),但是hash值可能会有冲突,所以还需要再调用equals方法进行二次比较。

switch中的表达式是不可以是boolean类型的。如果想转换为Boolean类型,只能是转换成字符串后再使用。


# 为什么说String是“不可变的”呢?


在介绍后面的字符串拼接之前,我觉得有必要先来说说为什么String是不可变的。

来看看这段程序:
public static void main(String[] args) { String s = "123"; System.out.println("s = " + s); s = "456"; System.out.println("s = " + s); }

输出:
s = 123s = 456

学过JVM的人都知道:

在Java中,对象在创建完后,对象内的成员变量是不能够就行改变的。
意思就是:

1.基本数据类型的值不能改变。
2.引用类型的变量不能指向其他的对象。
3.引用类型指向的对象的状态也不能改变。

那为什么上面的S变量能够被重新赋值呢?

首先我们需要区分对象和引用之间的关系:
对象是一块内存区,而引用只是一个4字节的数据,引用存放了要指向对象的地址,通过该地址来访问对象,s就是那个引用,它并不能代表对象,所以上面的s虽然是同一个引用,但是两次所指的对象并不是同一个。s=456,这行代码就已经是重新创建一个对象了。而原来的对象还保存在内存中。

在Java中对象本身是不能直接进行操作的,必须通过引用去访问对象,从而获取成员变量的值,再去改变成员变量。(切记,这里的对象并不只是指String对象)这也正是Java与C++的不同所在。

字符串操作方法:replaceFirst()、replaceAll()、replace(),也正是这种思想。
所以说String确实是不可变的。

为什么String要设置成不可变的呢 ?

直接看源码jdk1.8,在此之前首先需要知道String在jdk6到7是不一样的,我们不去纠结之前的版本,这里以1.7/1.8为例:
/** The value is used for character storage. */private final char value[];/** Cache the hash code for the string */private int hash; // Default to 0

源码已经解释得很清楚了hash成员变量就是为了缓存hashcode的,String的不可变性恰恰保证了hashcode的唯一性/性能优化。在很多地方都使用到了String,所以设计成不可变的,也是为了安全所考虑的,包括多线程安全以及其他重要安全问题。

还有一点需要知道的是:字符串常量池的需要。

当创建一个String对象时,假如此字符串值已经存在于常量池中,则不会创建一个新的对象,而是引用已经存在的对象。

如下面这一段代码:

String s = "ABCabc";System.out.println("s = " + s); //输出:ABCabcString s1 = "ABCabc";System.out.println("s1 = " + s1); //输出:ABCabc

一句理解它:新的String对象的引用s1会直接指向已经存在于字符串常量池中的“ABCabc”;
如果是:
String s2 = new String("ABCabc");

这样就不行了,这样的话
s1 != s2

为什么呢?

这句语意是:为变量s2开辟空间,然后直接将值写入空间。
这里的空间并不是字符串常量池,故不相等。

如果是使用String的equals方法,那么比较s1和s2就会返回true:

equals比较的是对象的值,并不是对象。


# 字符串池、常量池、intern


上面在讲解String的不可变性的时候,已经有说到了字符串常量池,这里就有必要进行介绍介绍:

ps:不好意思,字符串操作还要等一等,没有那么快就讲解到,由于本文是整体的解释String,所以篇幅可能是有点长的,觉得文章不错,可以收藏一波

字符串池(全局字符串池/string pool或者string literal pool):

池当然是用来存放内容的啦,那么这个内容在加进来时需要走什么流程呢?
内容在—>类加载完成—>经过验证(前面到这里都是准备阶段)—>准备阶段之后在堆中生成字符串对象实例—>然后将该字符串对象实例的引用值存到字符串池中。

PS:池中的引用值并不是具体的实例对象,具体实例对象是在堆内存空间中。切记!!!

常量池:有class文件常量池与运行时常量池。

class文件常量池:一看名称就能知道这个常量池是属于class文件中的一部分。
常量一般包括:

类和接口的全限定名
字段的名称和描述符
方法的名称和描述符

运行时常量池 /(runtime constant pool)
运行时常量池存放的是class文件常量池中的内容,如何加进去的呢?总该有个流程吧:

JVM在执行类的时候,必须有:加载—>连接(验证、准备、解析(把符号引用替换为直接引用,然后会去字符串池中查找))—>初始化。

在类加载到内存中后,class文件常量池中的内容就存放进运行时常量池中了。

而class文件常量池中的内容并不是对象的实例,而是字面量和符号引用。

所以运行时常量池存放的是对象的符号引用值。

本文重点在于String,所以常量池这块不做细究,有兴趣可以去看看相关材料。


# 字符串拼接的几种方式和区别/JDK1.8


回到本文重点了,前面虽然铺垫了这么多,但也很重要。

Java中字符串拼接有很多种方式,这里就介绍大家平常用的最多的几种:

1.使用“+”号 /String对“+”的重载2.使用String的方法concat 3.使用StringBuilder的append 4.使用StringBuffer的append

1.使用“+”号

String a = "123";String b = "456";String c = a + b;

实现原理:语法糖

将String转化为StringBuilder后,通过append()方法j进行处理。

2.使用String的方法concat


String a = "123";String b = "456";String c = a.concat(b);

实现原理:new String

首先来看看concat的源码:
public String concat(String str) { int otherLen = str.length(); if (otherLen == 0) { return this; } int len = value.length; char buf[] = Arrays.copyOf(value, len + otherLen); str.getChars(buf, len); return new String(buf, true);}

从源码可以看出,concat()方法是将两个字符串放入一个新的数组中,再将数组中的字符组装到一个新的String对象中去。

3.使用StringBuilder的append

StringBuilder a = new StringBuilder("123");String b = "456";String c = a.append(b);
实现原理:字符数组。

StringBuilder内部将封装了一个字符数组,append会直接拷贝字符到该字符数组中,并且该字符数组可以进行扩展。

4.使用StringBuffer的append


StringBuffer a = new StringBuffer("123");String b = "456";String c = a.append(b);

实现原理:与StringBuilder一样,不过StringBuffer是线程安全的。


 # String,StringBuffer与StringBuilder的区别?


上面已经完成了对字符串操作的介绍,但是对面涉及到了StringBuffer与StringBuilder,在大多情况下都会被问到这两个字符串变量,这里索性来探讨下这两者以及String的区别所在。

String和StringBuilder可以看成是一种,思想就是:new String。

为什么要有StringBuffer,将StringBuffer设计成线程安全的,主要还是为了解决上面两者所带来的性能问题,由于StringBuffer是同步的,所以相当于下一个操作并不会重新创建一个对象,而是使用池中的对象。

建议:在使用StringBuffer和StringBuilder的时候,不管数据量有多大,能够根据数据量事先预定设置好容量capacity,也是以一种减少开销的方法,毕竟扩容的开销有点大了,阅读过ArrayList的源码就知道这一点。一般在多线程环境下操作的还是对象本身,而不是生成新的对象时,还是建议使用StringBuffer,单线程下需要操作大量字符数据那么选择StringBuilder,除此之外一般都是使用String。

一般使用优先级:StringBuilder>StringBuffer>String。

具体的应用,并不一定要按照上面的规定进行选择,实际还是仁者见仁。

上面将完了字符串的拼接,这里将下String的另一种字符操作:

# 字符截取/JDK6与JDK7的不同。


截取方法:substring();
String a = "0123456";a = a.substring(1 , 4);System.out.println(a); //123

没错字符截取,还是要重新去new String,只不过这里与前面不同的是,这里是讲新的String对象的引用a去指向堆中的同一个字符数组。

JDK6 substring源码:

String(int offset, int count, char value[]) { this.value = value; this.offset = offset; this.count = count;}public String substring(int beginIndex, int endIndex) { return new String(offset + beginIndex, endIndex - beginIndex, value);}

上面也只是JDK6下的substring的实现原理。

来看看JDK7中的变化:
JDK6的时候,只是让新的String对象去指向原先的字符数组,而JDK7不同在于这个字符数组上,并不是指向原先的字符数组,而是重新在堆中创建一个新的字符数组,再去指向该新的字符数组。

JDK7 substring源码:
public String(char value[], int offset, int count) { this.value = Arrays.copyOfRange(value, offset, offset + count);}public String substring(int beginIndex, int endIndex) { int subLen = endIndex - beginIndex; return new String(value, beginIndex, subLen);}

为什么要这么设计呢?

如果是JDK6下的substring,要处理一个很长的字符串时,需要截取的只是一小段,却引用了整个字符串,被一直引用,就会导致GC无法进行回收,从而出现内存泄露的情况。

性能问题:内存浪费。

知识小鸡腿:String.valueOf() 解决了toString() 在参数为0的时候会报空指针异常的情况。

热文推荐

使用IntelliJ IDEA进行Java代码调试的技巧
JAVA设计模式之桥接模式
对程序员职业的一些建议
同时,分享一份Java面试资料给大家,覆盖了算法题目、常见面试题、JVM、锁、高并发、反射、Spring原理、微服务、Zookeeper、数据库、数据结构等等。
获取方式:点“在看”,关注公众号并回复 面试 领取。

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存